#! /bin/bash
##
## vmsbackup.sh
## Copyright 2008 Gerald D. Anderson
## Licensed under GPL v3, see including LICENSE file.
##
##  Version 1.0
##  Original: http://www.twobaldgeeks.com/blog/index.php/2007/08/06/vmware-how-do-i-love-thee-let-me-count-the-ways/
##  
## ** This is the culmination of work by a number of people, for more information see the above URL.  As of 4/13/2008 the 
##     Project will be hosted a google code with a google group for discussion.  Please refer to the following URLs:
##
##     Google Code project:  http://code.google.com/p/vmsbackup/
##     Google Group:  http://groups.google.com/group/vmsbackup
##
##  Customise these settings:
##  -------------------------
##  "BACKUP_DEST" where you want the backups to go
##  "VMWARE_CMD" the location of vmware-cmd
##  "DEBUG" true if you want to see verbose output, else false
##  "DAYS_TO_KEEP_TAR" how long before the script removes an old archive
##  "BACKUP_ACTIVE_ONLY=true" prevent inactive VMs from being included
##  "MODE=suspend" will suspend the VM before backup or
##  "MODE=stop" will stop it all together (untested)
##  "EXEMPTION_ARRAY" will exclude VMs in any of the specified directories
##  "REMOTE_ARCHIVES" if true, will rsync your tar files to a remote host.
##  "CLEAN_REMOTE_ARCHIVES" if true, will run rsync with the --delete parameter
##    deleting any archive files on remote host that don't exist on this host.
##  "REMOTE_ARCHIVE_URL" the remote path parameter for rsync.
##
##  Any Virtual Machines in the VMware Server inventory may now be backed up
##  by this script however the script WILL fail if you have any spaces in the
##  canonical path to the VM. It's now going to be far easier to change your
##  directory paths than it is to try and change the way this script handles
##  arrays. You can now change your paths and then setup a symlink to the vmx
##  file and directory so that VMware server is oblivious to the changed path.
##
##  Eg. For a VM located in /mnt/data/Virtual Machines/OS XYZ/OS XYZ.vmx you
##  can fix it by changing it to /mnt/data/Virtual_Machines/OS_XYZ/OS_XYZ.vmx
##  and then running the following commands to create the symlinks:
##      #ln -s /mnt/data/Virtual_Machines /mnt/data/Virtual\ Machines
##      #ln -s /mnt/data/Virtual_Machines/OS_XYZ /mnt/data/Virtual\ Machines/OS\ XYZ
##      #ln -s /mnt/data/Virtual_Machines/OS_XYZ/OS_XYZ.vmx /mnt/data/Virtual\ Machines/OS\ XYZ/OS\ XYZ.vmx
##  (This is also untested ;p)
##
##  Warning: It's now possible to have duplicate backup directories for VMs
##  with the same directory names. This will result in backups being
##  overwritten. This is an outstanding bug yet to be fixed most likely by
##  changing the directory naming method.
##
##  Thanks to Gerald Anderson (aka Left Bain/Hardaur) for the initial hard work!
##
 
BACKUP_DEST=/media/backup
VMWARE_CMD=/usr/bin/vmware-cmd
DEBUG=true
DAYS_TO_KEEP_TAR=6
BACKUP_ACTIVE_ONLY=false
MODE=suspend
REMOTE_ARCHIVES=true
CLEAN_REMOTE_ARCHIVES=true
REMOTE_ARCHIVE_URL=rsync.mydomain.com::backup/vmback
 
## Include any VMs in this array that you do NOT want backed up.  Use
## the directory name.  For example:  If your vm is in a directory called
## web0 then put 'web0' in the list.  Separate with spaces.
EXEMPTION_ARRAY=( )


###########################################################################
##  DO NOT EDIT BELOW
###########################################################################
 
##
## Create the backup directories if they do not exist
##
function doCheckDirectories
{
    ## if BACKUP_DEST does not  exist, create it
    if [ ! -d $BACKUP_DEST ]; then
 
        if [ "$DEBUG" = "true" ]; then
            echo $BACKUP_DEST does not exist, creating.
        fi
 
        mkdir $BACKUP_DEST
    fi
 
    ## if the archives directory does not  exist, create it
    if [ ! -d $BACKUP_DEST/archives ]; then
 
        if [ "$DEBUG" = "true" ]; then
            echo $BACKUP_DEST/archives does not exist, creating.
        fi
 
        mkdir $BACKUP_DEST/archives
    fi
 
    ## if the directories directory does not exist, create it
    if [ ! -d $BACKUP_DEST/directories ]; then
 
        if [ "$DEBUG" = "true" ]; then
            echo $BACKUP_DEST/directories does not exist, creating.
        fi
 
        mkdir $BACKUP_DEST/directories
 
    fi
}
 
##
## If this VM is in our exempt array, set VM_EXEMPT to skip entirely.
##
function doCheckExempt
{
    # iterate throught the array, if we get a match, set
    # VM_EXEMPT to true
    for check_vm in ${EXEMPTION_ARRAY[@]}; do
 
        if [ "$check_vm" = "$directoryName" ]; then
 
            if [ "$DEBUG" = "true" ]; then
                echo $vm is on the exception list, skipping.
            fi
 
            VM_EXEMPT=true
        fi
    done
}
 
##
## Set the displayName from the vmx file
##
function setDisplayName
{
    # get the line from the file
        displayLine=`grep displayName "$1"`
 
    # get the quoted name from it
    quotedName=${displayLine#*=}
 
    # and get rid of the quotes and spaces
    displayName=${quotedName//[\" ]/} #" &lt;- for syntax highlighting
 
    if [ "$DEBUG" = "true" ]; then
        echo $vm displayName is $displayName
    fi
}
 
##
## Set the directoryName from the path
##
function setDirectoryName
{
    # remove traling / and filename
    directoryPath=${vm%/*}
 
    # remove the begining of the path
    directoryName=${directoryPath##*/}
 
    if [ "$DEBUG" = "true" ]; then
        echo $vm directoryName is $directoryName
    fi
}
 
##
## suspend a VM if its running, skip it if not
##
function suspendVM
{
    # get the current VM state
    vmstate=`$VMWARE_CMD -q "$vm" getstate`
 
    if [ "$DEBUG" = "true" ]; then
        echo $displayName state is currently: $vmstate
    fi
 
    # if its running, suspend it, otherwise, move on.
    if [ "$vmstate" = "on" ]; then
 
        if [ "$DEBUG" = "true" ]; then
            echo suspending $displayName - MODE=$MODE . . .
        fi
 
        # make sure it actually suspends
        if [ `$VMWARE_CMD -q "$vm" $MODE hard` == 1 ]; then
 
            # track if it was running, so I can restart or not
            VM_WAS_RUNNING=true
 
            if [ "$DEBUG" = "true" ]; then
                echo $displayName Suspended at `date '+%F %T'`
            fi
 
            return
        else
            echo $vm DID NOT SUSPEND!! Exiting Program.
            exit 0
        fi
    else
        if [ "$DEBUG" = "true" ]; then
            echo $displayName was not originally active, no need to restart
        fi
    fi
}
 
##
## backup the VM
##
function doBackup
{
    # synchronise (update) all data to the directories tree
    if [ "$DEBUG" = "true" ]; then
        echo Backing up $displayName to $BACKUP_DEST/directories/$directoryName
        rsync -vax --progress --numeric-ids --delete "$directoryPath" "$BACKUP_DEST/directories/$directoryName"
    else
        rsync -ax --numeric-ids --delete "$directoryPath" "$BACKUP_DEST/directories/$directoryName"
    fi
}
 
##
## Resume the VM if it was running in the first place
##
function resumeVM
{
    if [ "$VM_WAS_RUNNING" = "true" ]; then
        if [ "$DEBUG" = "true" ]; then
            echo Resuming $displayName at `date '+%F %T'`
        fi
 
        if [ `$VMWARE_CMD -q "$vm" start` == 1 ]; then
 
            if [ "$DEBUG" = "true" ]; then
                echo $displayName restarted
            fi
        else
            echo $vm FAILED TO RESUME!! NOT Exiting Program.
            #exit 0
        fi
 
    else
        if [ "$DEBUG" = "true" ]; then
            echo $displayName wasnt running in the first place, not resuming.
        fi
    fi
}
 
##
## tgz up the directory for a more compressed and mobile backup.
##
function doTar
{
 
    fileName=backup_$displayName-`/bin/date +%G%m%d`.tgz    
 
    if [ "$DEBUG" = "true" ]; then
        echo taring up $displayName to $fileName
        tar -cvPpszf "$BACKUP_DEST/archives/$fileName" "$BACKUP_DEST/directories/$directoryName"
    else
        tar -cpPszf "$BACKUP_DEST/archives/$fileName" "$BACKUP_DEST/directories/$directoryName"
    fi
 
}
 
##
## Clean up any tars that are older than DAYS_TO_KEEP_TAR
##
function doCleanTar
{
    if [ "$DEBUG" = "true" ]; then
        echo Cleaning up tars older than $DAYS_TO_KEEP_TAR
        find "$BACKUP_DEST/archives" -name "backup_$displayName*.tgz" -ctime +$DAYS_TO_KEEP_TAR -exec rm -vf {} \;
    else
        find "$BACKUP_DEST/archives" -name "backup_$displayName*.tgz" -ctime +$DAYS_TO_KEEP_TAR -exec rm -vf {} \;
    fi
}

##
##  Copy my archives to a remote host via rsync
##
function doRemoteArchives
{
	if [ "$DEBUG" = "true" ]; then
		echo Copying archives to remote location.
		if [ "$CLEAN_REMOTE_ARCHIVES" = "true" ]; then
			rsync -vaxz --progress --stats --numeric-ids --delete "$BACKUP_DEST/archives/" "$REMOTE_ARCHIVE_URL"
	  else
			rsync -vaxz --progress --stats --numeric-ids "$BACKUP_DEST/archives/" "$REMOTE_ARCHIVE_URL"
		fi
	else
		if [ "$CLEAN_REMOTE_ARCHIVES" = "true" ]; then
			rsync -axz --numeric-ids --delete "$BACKUP_DEST/archives/" "$REMOTE_ARCHIVE_URL"
		else
			rsync -axz --numeric-ids "$BACKUP_DEST/archives/" "$REMOTE_ARCHIVE_URL"
		fi
	fi
}
 
##
## Main Loop, iterate through the VMs and handle them apprpriately
##
 
# make sure we have the appropriate directories for backups
doCheckDirectories
 
#for vm_noncanon in $(vmware-cmd -l); do
$VMWARE_CMD -l | while read file; do
 
    #This one line below is necessary to treat vm_noncanon as one string instead of multiple
    vm_noncanon=$file
 
   #Get the canonical path to the Virtual Machine (remove any symlinks)
    vm=`readlink -f "$vm_noncanon"`
 
    # reset variables for this VM
    VM_WAS_RUNNING=false
    VM_EXEMPT=false
 
    if [ "$DEBUG" = "true" ]; then
        echo ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
        echo Current VM is $vm
    fi
 
        # get my display and directory names, for use in tar name, etc.
        setDisplayName "$vm"
        setDirectoryName "$vm"
 
        # check to see if current vm should be exempted
        doCheckExempt
 
        # only back up if it is not on the exempt list
        if [ "$VM_EXEMPT" = "false" ]; then
 
            # suspend my VM if its running
            suspendVM
 
            if [ "$BACKUP_ACTIVE_ONLY" = "false" ] || [ "$VM_WAS_RUNNING" = "true" ]; then
                # actually do the directory backup
                doBackup
 
                # resume the VM if it was running to begin with
                resumeVM
 
                # tar it up
                 doTar
 
                # and finally, clean up my old tars
                doCleanTar
            fi
        fi
 
done

if [ "$REMOTE_ARCHIVES" = "true" ]; then
	doRemoteArchives
fi
